/**
 * Copyright (C) 2010 Hamburg Sud
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hamburgsud.log4testing.core.output.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Node;

import com.hamburgsud.log4testing.core.configuration.Log4TestingConfiguration;
import com.hamburgsud.log4testing.core.data.TestCase;
import com.hamburgsud.log4testing.core.data.TestObject;
import com.hamburgsud.log4testing.core.data.TestSuite;
import com.hamburgsud.log4testing.core.output.TestLogWriter;
import com.hamburgsud.log4testing.core.output.writer.Writer;

public class WriterLoader<T extends TestObject> {

	final private static Logger logger = Logger.getLogger(TestLogWriter.class);

	static private String SETTER_PREFIX = "set";
	static private String XPATH_TESTCASE_CLASSES = "/log4testing/writer/testcase/class";
	static private String XPATH_TESTSUITE_CLASSES = "/log4testing/writer/testsuite/class";
	static private String XPATH_SPECIFIC_WRITER_PROPERTIES = "../properties/*";
	static private String XPATH_GENERAL_WRITER_PROPERTIES = "../../properties/*";

	protected Class<?> genericClass;

	private WriterLoader() {
		super();
	};

	public WriterLoader(final Class<?> genericClass) {
		this();
		this.genericClass = genericClass;
	}

	private Collection<String> getMethodParams(
			final Collection<Node> writerPropNodes) {
		final Collection<String> methodParams = new ArrayList<String>();
		for (final Node methodNode : writerPropNodes) {
			final String nodeValue = methodNode.getText();
			methodParams.add(nodeValue);
		}
		return methodParams;
	}

	private Collection<String> getMethodNames(
			final Collection<Node> writerPropNodes) {
		final Collection<String> methodNames = new ArrayList<String>();
		for (final Node methodNode : writerPropNodes) {
			final String nodeName = methodNode.getName();
			final StringBuilder methodName = new StringBuilder(SETTER_PREFIX);
			methodName.append(nodeName.substring(0, 1).toUpperCase());
			methodName.append(nodeName.substring(1, nodeName.length()));
			methodNames.add(methodName.toString());
		}
		return methodNames;
	}

	@SuppressWarnings("unchecked")
	private Collection<Node> getWriterPropNodes(final Node writerClassNode) {
		final List<Node> writerPropNodes = new ArrayList<Node>();
		writerPropNodes.addAll(writerClassNode
				.selectNodes(XPATH_SPECIFIC_WRITER_PROPERTIES));
		writerPropNodes.addAll(writerClassNode
				.selectNodes(XPATH_GENERAL_WRITER_PROPERTIES));
		return writerPropNodes;
	}

	private String substituteVariables(String param) {
		param = substituteEnvVariables(param);
		param = substituteSystemProperties(param);
		return param;
	}

	private String substituteSystemProperties(final String param) {
		final StrLookup systemLookup = StrLookup.systemPropertiesLookup();
		final StrSubstitutor substitutor = new StrSubstitutor(systemLookup);
		return substitutor.replace(param);
	}

	private String substituteEnvVariables(final String param) {
		final Map<String, String> envVariables = System.getenv();
		return StrSubstitutor.replace(param, envVariables);
	}
	
	private String getParameter(final Iterator<String> params) {
		String param = "";
		if (params.hasNext()) {
			param = params.next();
			param = substituteVariables(param);
		} else {
			param = "";
		}
		return param;
	}

	private void callSetters(final Writer<T> writer, final Node writerClassNode) {
		final Collection<Node> writerPropNodes = getWriterPropNodes(writerClassNode);
		final Collection<String> methodNames = getMethodNames(writerPropNodes);
		final Collection<String> methodParams = getMethodParams(writerPropNodes);
		final Iterator<String> params = methodParams.iterator();
		for (final String methodName : methodNames) {
			try {
				final Method method = writer.getClass().getMethod(methodName,
						String.class);
				final String param = getParameter(params);
				method.invoke(writer, param);
			} catch (SecurityException e) {
				logger.error("The method is not accessible. Please check "
						+ "that the method is public and that the classloader "
						+ "has the rights to access this method", e);
			} catch (NoSuchMethodException e) {
				logger.error("There is no method with the given nodename", e);
			} catch (NullPointerException e) {
				logger.error("The method name is null", e);
			} catch (IllegalArgumentException e) {
				logger.error("The method must have exactly one parameter "
						+ "of the type String", e);
			} catch (IllegalAccessException e) {
				logger.error("Please check that the method is accessible", e);
			} catch (InvocationTargetException e) {
				logger.error("The underlying method has thrown a "
						+ "exception.", e);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private Writer<T> getWriterInstance(final Node writerClassNode) {
		final String className = writerClassNode.getText();
		Writer<T> writer = null;
		try {
			writer = (Writer<T>) Class.forName(className).newInstance();
			callSetters(writer, writerClassNode);
		} catch (InstantiationException e) {
			logger.error("Class could not be instantatied. "
					+ "Please check that the class is a concrete class "
					+ "with a nullary constructor.", e);
		} catch (IllegalAccessException e) {
			logger.error("Please check that the class and "
					+ "the nullary constructor are public.", e);
		} catch (ClassNotFoundException e) {
			logger.error("Class could not be found. Please check that "
					+ "the class is in the classpath and that the "
					+ "fully qualified class name is correct.", e);

		} catch (ClassCastException e) {
			logger.error(
					"A Writer class must extend the class "
							+ Writer.class.getName(), e);
		}
		return writer;
	}

	private List<Writer<T>> getWriters(final List<Node> writerClassNodes) {
		final List<Writer<T>> writers = new ArrayList<Writer<T>>();
		for (final Node writerClassNode : writerClassNodes) {
			final Writer<T> writer = (Writer<T>) getWriterInstance(writerClassNode);
			if (writer != null) {
				writers.add(writer);
			}
		}
		return writers;
	}

	private String getXPathOfWriterClasses() {
		String xpath = "";
		if (genericClass.isAssignableFrom(TestCase.class)) {
			xpath = XPATH_TESTCASE_CLASSES;
		} else if (genericClass.isAssignableFrom(TestSuite.class)) {
			xpath = XPATH_TESTSUITE_CLASSES;
		}
		return xpath;
	}

	@SuppressWarnings("unchecked")
	public List<Writer<T>> getWriters() {
		final Document config = Log4TestingConfiguration.getConfig();
		final String xpath = getXPathOfWriterClasses();
		final List<Node> writerClasses = config.selectNodes(xpath);
		return getWriters(writerClasses);
	}

}
